package org.example.springoauth2.securitycommon.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author hzq
 * @date 2021/10/10 15:39
 */
@Slf4j
@ConfigurationProperties(prefix = "spring.security")
public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware {

    public static final Pattern PATTERN = Pattern.compile("\\{(.*?)}");
    public static final String ASTERISK = "*";
    public static final String SEPARATOR = "|";
    public static final PathMatcher PATHMATCHER = new AntPathMatcher();

    private ApplicationContext applicationContext;
    @Getter
    @Setter
    private List<String> ignoreUrls = new ArrayList<>();
    @Getter
    @Setter
    private boolean innerCheck = true;

    @Override
    @SneakyThrows
    public void afterPropertiesSet() {

        RequestMappingHandlerMapping requestMappingHandlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods();

        for (RequestMappingInfo info : map.keySet()) {

            HandlerMethod handlerMethod = map.get(info);

            // 1. 首先获取方法上 @Inner 注解
            Inner methodInner = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
            if (null != methodInner) {
                info.getPatternsCondition().getPatterns().forEach(url -> this.filterPath(url, info, map, methodInner));
                continue;
            }

            // 2. 当方法上不包含 @Inner 注解则获取该类上的注解
            Inner controllerInner = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);
            if (controllerInner == null) {
                continue;
            }

            // 3. 当类上包含 @Inner 注解 判断handlerMethod 是否包含在 inner 类中
            Class<?> beanType = handlerMethod.getBeanType();
            Method[] methods = beanType.getDeclaredMethods();
            Method method = handlerMethod.getMethod();
            if (ArrayUtil.contains(methods, method)) {
                info.getPatternsCondition().getPatterns().forEach(url -> filterPath(url, info, map, controllerInner));
            }
        }
    }

    /**
     * 过滤 Inner 设置
     * <p>
     * 0. 暴露安全检查
     * 1. 路径转换： 如果为restful(/xx/{xx}) --> /xx/* ant 表达式
     * 2. 构建表达式：允许暴露的接口|允许暴露的方法类型,允许暴露的方法类型 URL|GET,POST,DELETE,PUT
     * </p>
     *
     * @param url  mapping路径
     * @param info 请求犯法
     * @param map  路由映射信息
     */
    private void filterPath(String url, RequestMappingInfo info, Map<RequestMappingInfo, HandlerMethod> map, Inner inner) {
        // 安全检查
        if (innerCheck) {
            security(url, info, map);
        }
        List<String> methodList = info.getMethodsCondition().getMethods().stream().map(RequestMethod::name).collect(Collectors.toList());
        String resultUrl = ReUtil.replaceAll(url, PATTERN, ASTERISK);
        if (CollUtil.isEmpty(methodList)) {
            ignoreUrls.add(String.format("%s|%s", resultUrl, inner.value()));
        } else {
            ignoreUrls.add(String.format("%s|%s|%s", resultUrl, CollUtil.join(methodList, StrUtil.COMMA), inner.value()));
        }
    }

    /**
     * 针对PathVariable 请求安全检查。增加启动好使影响启动效率 请注意
     *
     * @param url 接口路径
     * @param rq  当前请求的元信息
     * @param map springMvc 接口列表
     */
    private void security(String url, RequestMappingInfo rq, Map<RequestMappingInfo, HandlerMethod> map) {
        // 判断 URL 是否是 rest path 形式
        if (!StrUtil.containsAny(url, StrUtil.DELIM_START, StrUtil.DELIM_END)) {
            return;
        }

        for (RequestMappingInfo info : map.keySet()) {
            Set<RequestMethod> methods = info.getMethodsCondition().getMethods();
            // 如果请求方法不匹配跳过
            if (!CollUtil.containsAny(methods, rq.getMethodsCondition().getMethods())) {
                continue;
            }

            // 如果请求方法路径匹配
            Set<String> patterns = info.getPatternsCondition().getPatterns();
            for (String pattern : patterns) {
                // 跳过自身
                if (StrUtil.equals(url, pattern)) {
                    continue;
                }

                if (PATHMATCHER.match(url, pattern)) {
                    HandlerMethod rqMethod = map.get(rq);
                    HandlerMethod infoMethod = map.get(info);
                    log.error("@Inner 标记接口 ==> {}.{} 使用不当，会额外暴露接口 ==> {}.{} 请知悉",
                            rqMethod.getBeanType().getName(), rqMethod.getMethod().getName(),
                            infoMethod.getBeanType().getName(), infoMethod.getMethod().getName());
                }
            }
        }
    }

    public void registryAntMatchers(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
        for (String url : getIgnoreUrls()) {
            List<String> strings = StrUtil.splitTrim(url, SEPARATOR);

            // /home
            // /home|true
            // 仅配置对外暴露的URL ，则注册到 spring security的为全部方法
            if (strings.size() == 1 || strings.size() == 2) {
                registry.antMatchers(strings.get(0)).permitAll();
                continue;
            }

            // /home|GET|true // /home|GET,POST|true
            // 当配置对外的URL|GET,POST 这种形式，则获取方法列表 并注册到 spring security
            if (strings.size() == 3) {
                for (String method : StrUtil.split(strings.get(1), StrUtil.COMMA)) {
                    registry.antMatchers(HttpMethod.valueOf(method), strings.get(0)).permitAll();
                }
                continue;
            }

            log.warn("{} 配置无效，无法配置对外暴露", url);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
